extern crate proc_macro;

use proc_macro::{TokenStream};
use quote::{quote, format_ident};
use syn::{parse_quote, parse_macro_input, DeriveInput, Data, Fields, Field, FieldValue, token::Comma, punctuated::Punctuated, ItemFn};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;
    let builder_name = format_ident!("{}Builder", name.to_string());

    let fields: Vec<Field> = get_data_fields(&input.data).iter().cloned().collect();
    let mut builder_fields: Punctuated<_, Comma> = fields.iter().cloned().collect();
    builder_fields.iter_mut().for_each(|mut f| {
        let ty = f.ty.clone();
        f.ty = parse_quote!{ Option<#ty> };
    });

    let builder_init: Punctuated<FieldValue, Comma> = fields.iter().cloned().map(|f| {
        let ident = f.ident.unwrap();
        let fv: FieldValue = parse_quote!{ #ident : None };
        fv
    }).collect();

    let builder_setters: Vec<ItemFn> = fields.iter().cloned().map(field_to_setter).collect();

    let builder_build: Punctuated<FieldValue, Comma> = fields.iter().cloned().map(|f| {
        let ident = f.ident.unwrap();
        let ident_name = ident.to_string();
        let fv : FieldValue = parse_quote!{ #ident : self.#ident.as_ref().ok_or(format!("{} is not build", #ident_name))?.clone() };
        fv
    }).collect();

    let expand = quote! {
        pub struct #builder_name { 
            #builder_fields
        }

        impl #builder_name {
            #(#builder_setters)*

            pub fn build(&mut self) -> Result<#name, Box<dyn ::std::error::Error>> {
                Ok(#name {
                    #builder_build
                })
            }
        }

        impl #name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #builder_init
                }
            }
        }
    };

    TokenStream::from(expand)
}

fn get_data_fields(data: &Data) -> Punctuated<Field, Comma> {
    match *data {
        Data::Struct(ref data) => {
            match data.fields {
                Fields::Named(ref fields) => fields.named.clone(),
                _ => Punctuated::new()
            }
        },
        _ => {
            Punctuated::new()
        }
    }
}

fn field_to_setter (field: Field) -> ItemFn {
    let name = field.ident.unwrap();
    let ty = field.ty;

    parse_quote! {
        pub fn #name(&mut self, val: #ty) -> &mut Self {
            self.#name = Some(val);
            self
        }
    }
}